package com.pic.mgt.monitor.physicalmonitor;

import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Vector;

import com.den_4.inotify_java.EventQueueFull;
import com.den_4.inotify_java.Inotify;
import com.den_4.inotify_java.InotifyEvent;
import com.den_4.inotify_java.InotifyEventListener;
import com.den_4.inotify_java.NativeInotify;
import com.den_4.inotify_java.enums.Event;
import com.den_4.inotify_java.exceptions.InotifyException;
import com.pic.mgt.log.IgxLog;
import com.pic.mgt.utils.LogUtils;

/**
 * [檔案監控實作]，採用INotify-Java API，適用於 GNU-Linux-x86,GNU-Linux-x86_64bits，
 * 底層使用 Linux native binary Inotify (Linux 2.6.13 以上皆有提供)。 Linux 支援的Inotify
 * 監控事件類型請參閱 Linux Inotify 說明。支援監控事件類型：FILE_Generate,
 * FILE_CREATE,FILE_DELETE,FILE_MOTIFY,FILE_CLOSING, FILE_CLOSE_NO_WRITE,
 *  FILE_CLOSE_WRITE, FILE_OPEN。
 * @author 洪政緯
 *
 */
public class InotifyFileMonitor implements FileMonitor {
	// 監控相關設定
	private FileMonitorStatus monitorStatus = FileMonitorStatus.MONITOR_INITIAL;
	private List<FilenameFilter> filenameFilterList;
	private Map<String, MonitorPathInfo> monitorPathInfoMap;
	private Event[] registEvents;
	private Vector<FileMonitorEvent> fileMonitorEventsVector;
	
	private Queue<MonitoredFile> monitoredFileQueue;
	
	private InotifyEventListener inotifyFileListener;
	private Inotify inotify;
	
	public InotifyFileMonitor() {
		this.monitoredFileQueue = new LinkedList<MonitoredFile>();
		this.fileMonitorEventsVector = new Vector<FileMonitorEvent>();	
		
		inotifyFileListener = new InotifyFileListener();
		filenameFilterList = new LinkedList<FilenameFilter>();
		monitorPathInfoMap = new HashMap<String, MonitorPathInfo>();
	}

	@Override
	public synchronized FileMonitorStatus getMonitorStatus() {
		return this.monitorStatus;
	}

	@Override
	public List<MonitoredFile> readMonitoredList()
								throws FileMonitorStopException {
		synchronized (this.monitoredFileQueue) {
			if (!this.monitorStatus.equals(FileMonitorStatus.MONITOR_STARTED)) {
				throw new FileMonitorStopException(
						"File monitor has not started yet.");
			}
			
			if (this.monitoredFileQueue.isEmpty()) {
				try {
					this.monitoredFileQueue.wait();
				} catch (InterruptedException e) {
					// 可能為不明原因被OS停止
					try {
						throw new FileMonitorStopException(
								"File monitor has been stopped. Because of unknown reason.", e);
					} catch (FileMonitorStopException e1) {
						// ********************** Log **********************
						LogUtils.error(this, IgxLog.messageDetailsLog, e1);
						// ********************** Log **********************
						
						throw e1;
					}
				}
			}		
			/* 如果在等待(this.monitoredFileQueue.wait())之後，Queue 內仍沒有資料，表示叫用者已呼叫
			 * stopMonitor()方法停止檔案監控。
			 */
			if (this.monitoredFileQueue.isEmpty()) {
				try {
					throw new FileMonitorStopException(
							"File monitor has been stopped. Because of calling watch close.");
				} catch (FileMonitorStopException e) {
					// ********************** Log **********************
					if (IgxLog.messageDetailsLog.isDebugMode()) {
						IgxLog.messageDetailsLog.debug(this, e.getMessage());
					}
					// ********************** Log **********************
					
					throw e;
				}
			}
			
			List<MonitoredFile> monitoredFileList = new LinkedList<MonitoredFile>();
			while(!this.monitoredFileQueue.isEmpty()) {
				monitoredFileList.add(this.monitoredFileQueue.poll());
			}
			
			return monitoredFileList;
		}
	}

	@Override
	public synchronized void removeAllFilenameFilter() {
		if (this.monitorStatus != FileMonitorStatus.MONITOR_STARTED) {
			this.filenameFilterList.clear();
		}
	}

	@Override
	public synchronized void addFilenameFilter(FilenameFilter filenameFilter) {
		if (this.monitorStatus != FileMonitorStatus.MONITOR_STARTED) {
			this.filenameFilterList.add(filenameFilter);
		}
	}

	@Override
	public synchronized void addMonitorPath(String monitorPathStr) throws FileMonitorException {
		// 如果目錄不存在則會拋出例外
		if (!(new File(monitorPathStr)).exists()) {
			throw new FileMonitorException("The monitor path '" + 
												monitorPathStr + "' does not exist.");
		}
		
		// 要避免重覆加入以至 MonitorPathInfo被覆蓋掉
		if (!this.monitorPathInfoMap.containsKey(monitorPathStr)) {
			this.monitorPathInfoMap.put(monitorPathStr,
										new MonitorPathInfo(monitorPathStr,
															-1));
		}
		
		// ********************** Log **********************
		if (IgxLog.messageDetailsLog.isDebugMode()) {
			IgxLog.messageDetailsLog.debug(this,
					"Added a folder monitor path '{}'.", monitorPathStr);
		}
		// ********************** Log **********************
		
		// 如果是已經開始監控的狀態下,則直接將此 monitorPah 加入 Inotify 的 watch監控
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			// 如果該Path還沒有被監控才加入監控,反之則不需再做一次
			MonitorPathInfo monitorPathInfo = this.monitorPathInfoMap.get(monitorPathStr);
			if (-1 == monitorPathInfo.getInotifyWatchDescriptor()) {
				// throws FileMonitorException
				startWatchMonitorPath(monitorPathStr);
			}
		}
	}
	
	@Override
	public synchronized void removeMonitorPath(String monitorPathStr) {
		// 如果已經在監控的狀態下,必須先停止 Inotify 的 watch
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			stopWatchMonitorPath(monitorPathStr);
		}
		
		this.monitorPathInfoMap.remove(monitorPathStr);
	}
	
	@Override
	public synchronized void removeAllMonitorPath() {
		Set<String> monitorPathStrSet = this.monitorPathInfoMap.keySet();
		for (String monitorPathStr : monitorPathStrSet) {
			removeMonitorPath(monitorPathStr);
		}
	}
	
	@Override
	public synchronized void startWatchMonitorPath(String monitorPathStr)
			throws FileMonitorException {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			MonitorPathInfo monitorPathInfo = null;
			try {
				monitorPathInfo = this.monitorPathInfoMap.get(monitorPathStr);
				if (null != monitorPathInfo) {
					// throws InotifyException
					int watchDescriptor = addInotifyWatch(monitorPathInfo.getMonitorPathStr());
					
					monitorPathInfo.setInotifyWatchDescriptor(watchDescriptor);
				}
			} catch (InotifyException e) {
				try {
					throw new FileMonitorException(
							"Adding monitor path '" + monitorPathInfo.getMonitorPathStr() +
							"' watch error.", e);
				} catch (FileMonitorException e1) {
					// ********************** Log **********************
					LogUtils.error(this, IgxLog.messageDetailsLog, e1);
					// ********************** Log **********************
					
					throw e1;
				}
			}
		}
	}
	
	@Override
	public synchronized void stopWatchMonitorPath(String monitorPathStr) {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			MonitorPathInfo monitorPathInfo = this.monitorPathInfoMap.get(monitorPathStr);
			if (null != monitorPathInfo) {
				if (null != this.inotify && null != this.inotifyFileListener) {
					if (monitorPathInfo.getInotifyWatchDescriptor() != -1) {
						// 若InotifyWatchDescriptor為-1表示該目錄並沒有在被監控中
						try {
							this.inotify.removeListener(
									monitorPathInfo.getInotifyWatchDescriptor(),
								    this.inotifyFileListener);
						} catch (IllegalArgumentException e) {
							// 發生 IllegalArgumentException 表示被監控的目錄已經不在了
							// 忽略,不需做對應的處理
						}
					}
				}
				monitorPathInfo.setInotifyWatchDescriptor(-1);
			}
		}
	}
	
	@Override
	public synchronized void stopWatchAllMonitorPath() {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			if (null != this.inotify) {
				Set<String> monitorPathStrSet = this.monitorPathInfoMap.keySet();
				for (String monitorPathStr : monitorPathStrSet) {
					stopWatchMonitorPath(monitorPathStr);
				}
			}
		}
	}
	
	@Override
	public synchronized void setMonitorEvent(FileMonitorEvent... monitorEvents) {
		if (this.monitorStatus.equals(FileMonitorStatus.MONITOR_STARTED)) {
			return;
		}
		
		this.fileMonitorEventsVector.clear();
		for(FileMonitorEvent event : monitorEvents) {
			this.fileMonitorEventsVector.add(event);
		}
	}

	@Override
	public synchronized void startMonitor() throws FileMonitorException {
		if (this.monitorStatus.equals(FileMonitorStatus.MONITOR_STARTED)) {
			return;
		}
		
		try {
			// 如果尚未設定監控目錄和事件，則拋出例外
			if (this.monitorPathInfoMap.isEmpty() || this.fileMonitorEventsVector.isEmpty()) {
				throw new FileMonitorException("Monitor path and events are not provided.");
			}
		} catch (FileMonitorException e1) {
			// ********************** Log **********************
			IgxLog.messageDetailsLog.error(this, e1.getMessage());
			// ********************** Log **********************
			
			throw e1;
		}
		
		this.monitoredFileQueue.clear();
		
		// 取得轉換後的事件類型
		registEvents = mappingEvents();
		if (registEvents == null) {
			throw new FileMonitorException("do not find any matched event kind.");
		}
		
		try {
			// load native inotify component
			try {
				NativeInotify.loadLibrary(null);
				// throws InotifyException
				inotify = new Inotify();
				
			} catch (UnsatisfiedLinkError e) {
				throw new FileMonitorException(
						"Loading native Inotify component error.", e);
			} catch (InotifyException e) {
				throw new FileMonitorException(
						"Loading native Inotify component error.", e);
			}
		} catch (FileMonitorException e1) {
			// ********************** Log **********************
			LogUtils.error(this, IgxLog.messageDetailsLog, e1);
			// ********************** Log **********************
			
			throw e1;
		}
		
		this.monitorStatus = 
			FileMonitor.FileMonitorStatus.MONITOR_STARTED;
		
		// ********************** Log **********************
		if (IgxLog.messageDetailsLog.isInfoMode()) {
			IgxLog.messageDetailsLog.info(this, "Started file monitor.");
		}
		// ********************** Log **********************
		
		
		// 將Map內的monitor path 全部設定/增加到Inotify路徑監控
		Set<String> monitorPathStrSet = this.monitorPathInfoMap.keySet();
		for (String monitorPathStr : monitorPathStrSet) {
			// throws FileMonitorException
			startWatchMonitorPath(monitorPathStr);
		}
	}
	
	
	
	/**
	 * 
	 * @param monitorPath
	 * @return watchDescriptor
	 * @throws InotifyException
	 */
	private int addInotifyWatch(String monitorPath) throws InotifyException {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			if (null != this.inotify && null != this.registEvents && null != monitorPath) {
				// throws InotifyException
				int watchDescriptor = this.inotify.addWatch(monitorPath, registEvents);
				
				this.inotify.addListener(watchDescriptor, this.inotifyFileListener);
				
				// 當完成了 addWatch() 和  addListener()方法，即開始做檔案的監控
				
				return watchDescriptor;
			}
		}
		
		return -1;
	}
	
	@Override
	public synchronized void stopMonitor() {
		if (!this.monitorStatus.equals(FileMonitorStatus.MONITOR_STARTED)) {
			return;
		}
		
		if (this.inotify != null) {
			stopWatchAllMonitorPath();
			
			this.inotify.destroy();
			this.inotify = null;
			
			synchronized (this.monitoredFileQueue) {
				this.monitorStatus =
					FileMonitor.FileMonitorStatus.MONITOR_STOPPED;
				
				this.monitoredFileQueue.notify(); // 解除呼叫 readMonitoredList()方法所產生的等待
			}
			
			// ********************** Log **********************
			if (IgxLog.messageDetailsLog.isInfoMode()) {
				IgxLog.messageDetailsLog.info(this, "Stopped file monitor.");
			}
			// ********************** Log **********************
		}
	}
	
	private void putElementInQueue(MonitoredFile monitoredFile) {
		synchronized (this.monitoredFileQueue) {
			this.monitoredFileQueue.offer(monitoredFile);
			this.monitoredFileQueue.notify();
		}
	}
	
	/**
	 * 
	 * @return
	 */
	@SuppressWarnings(value={"unchecked"})
	private Event[] mappingEvents() {		
		List<Event> eventsList = new LinkedList<Event>();
		Iterator<FileMonitorEvent> iter = this.fileMonitorEventsVector.iterator();
		while(iter.hasNext()){
			FileMonitorEvent tmp = iter.next();
			switch(tmp){
				case FILE_Generate:
					// FILE_Generate 於本實作中對應到 Close_Write
					eventsList.add(Event.Close_Write);
					break;
				case FILE_CREATE:
					eventsList.add(Event.Create);
					break;
				case FILE_DELETE:
					eventsList.add(Event.Delete);
					break;
				case FILE_MOTIFY:
					eventsList.add(Event.Modify);
					break;
				case FILE_CLOSING:
					eventsList.add(Event.Close);
					break;
				case FILE_CLOSE_NO_WRITE:
					eventsList.add(Event.Close_No_Write);
					break;
				case FILE_CLOSE_WRITE:
					eventsList.add(Event.Close_Write);
					break;
				case FILE_OPEN:
					eventsList.add(Event.Open);
					break;
			}
		}
		if (eventsList.isEmpty()) {
			return null;
		}
		else {
			@SuppressWarnings(value={"unchecked"})
			Event[] events = new Event[eventsList.size()];
			eventsList.toArray(events);
			return events;
		}		
	}
	
	
	/**
	 * [INotify 檔案監控事件監聽]
	 * @author 洪政緯
	 */
	private class InotifyFileListener implements InotifyEventListener {
		@Override
		public void filesystemEventOccurred(InotifyEvent inotifyEvent) {
			/*
			 * 從 InotifyEvent 獲取的 Name 和 ContextualName　會因為事件類型的
			 * 不同而可能得到 Null值。例如：Open 事件，可能會有 null值；而 CLOSE_WRITE
			 * 則不會有 Null 值。
			 */
			// 監控到的檔案(僅檔名不含路徑)
			String monitoredFilename = inotifyEvent.getName();
			
			// 監控到的檔案(絕對路徑含檔名)
			String monitoredFullFilename = inotifyEvent.getContextualName();
			
			// 取得事件類型
			Event[] monitoredINotifyEvents = Event.maskToEvents(inotifyEvent.getMask());
			
			// 如果 監控到的檔名為 null 則忽略之 --------
			if (null == monitoredFilename) {
				return;
			}
			
			// ********************** Log **********************
			if (IgxLog.messageDetailsLog.isInfoMode()) {
				IgxLog.messageDetailsLog.info(InotifyFileMonitor.this, "Monitored a file motion. Filename: '{}'  Event: '{}'",
						monitoredFullFilename, Arrays.toString(monitoredINotifyEvents));
			}
			// ********************** Log **********************
			
			// 如果使用 file name filter, 則呼叫 filter進行檔名過濾
			if (!filenameFilterList.isEmpty()) {
				boolean acceptable = false;
				synchronized (InotifyFileMonitor.this) {
					for(FilenameFilter filenameFilter : filenameFilterList){
						if (filenameFilter.
								 accept(new File(monitoredFullFilename.trim()),
											monitoredFilename.trim())) {
							acceptable = true;
							break;
						}
					}	
				}
				if (!acceptable) {
					// ********************** Log **********************
					if (IgxLog.messageDetailsLog.isInfoMode()) {
						IgxLog.messageDetailsLog.info(InotifyFileMonitor.this, "'{}' has been ignored by filter.",
								monitoredFullFilename);
					}
					// ********************** Log **********************
					
					return;
				}
			}
			
			
			// 取得事件類型的第一個 INotfy Event為主
			Event monitoredINotifyEvent = monitoredINotifyEvents[0];
			
			// 取得事件類型(將 INotfy Event 對應成 FileMonitor Event)
			FileMonitorEvent eventType = null;
			if (monitoredINotifyEvent.equals(Event.Create)) {
				eventType = FileMonitorEvent.FILE_CREATE;
			}
			else if (monitoredINotifyEvent.equals(Event.Delete)) {
				eventType = FileMonitorEvent.FILE_DELETE;
			}
			else if (monitoredINotifyEvent.equals(Event.Modify)) {
				eventType = FileMonitorEvent.FILE_MOTIFY;
			}
			else if (monitoredINotifyEvent.equals(Event.Close)) {
				eventType = FileMonitorEvent.FILE_CLOSING;
			}
			else if (monitoredINotifyEvent.equals(Event.Close_No_Write)) {
				eventType = FileMonitorEvent.FILE_CLOSE_NO_WRITE;
			}
			else if (monitoredINotifyEvent.equals(Event.Close_Write)) {
				/* FileMonitorEvent.File_Generate 於本實作中，對應到的是 Close_Write 事件，
				 * 若叫用端有採用 FileMonitorEvent.File_Generate 事件，以其為優先。
				 */
				if (fileMonitorEventsVector.contains(FileMonitorEvent.FILE_Generate)) {
					eventType = FileMonitorEvent.FILE_Generate;
				}
				else {
					eventType = FileMonitorEvent.FILE_CLOSE_WRITE;
				}
			}
			else if (monitoredINotifyEvent.equals(Event.Open)) {
				eventType = FileMonitorEvent.FILE_OPEN;
			}
			putElementInQueue(
					new MonitoredFile(monitoredFullFilename, eventType, inotifyEvent.aboutDirectory()));
		}
		
		@Override
		public void queueFull(EventQueueFull eventQueueFull) {
			// ********************** Log **********************
			IgxLog.messageDetailsLog.error(InotifyFileMonitor.this, "An file monitor overflow event occured. Some of files motion maybe lost.");
			// ********************** Log **********************
			
			putElementInQueue(
					new MonitoredFile(null, FileMonitorEvent.EVENTOVERFLOW));
		}
	}
	
	private class MonitorPathInfo {
		private String monitorPathStr;
		private int inotifyWatchDescriptor = -1;
		
		private MonitorPathInfo(String monitorPath) {
			this.monitorPathStr = monitorPath;
		}
		
		private MonitorPathInfo(String monitorPath, int watchDescriptor) {
			this.inotifyWatchDescriptor = watchDescriptor;
			this.monitorPathStr = monitorPath;
		}
		
		// --- getter and setter ---

		public String getMonitorPathStr() {
			return monitorPathStr;
		}

		public void setMonitorPathStr(String monitorPathStr) {
			this.monitorPathStr = monitorPathStr;
		}

		public int getInotifyWatchDescriptor() {
			return inotifyWatchDescriptor;
		}

		public void setInotifyWatchDescriptor(int inotifyWatchDescriptor) {
			this.inotifyWatchDescriptor = inotifyWatchDescriptor;
		}
	}
}
